import random, time, pygame, sys
from pygame.locals import *  # 导pygame内定义的一些常量
FPS = 25  # 每秒传输帧数(刷新率)，此处一秒内在屏幕上连续投射出24张静止画面
WINDOWWIDTH = 640  # 窗口宽
WINDOWHEIGHT = 480  # 窗口高
BOXSIZE = 20  # 游戏框大小
BOARDWIDTH = 10  # 游戏框宽度
BOARDHEIGHT = 20  # 游戏框高度
BLANK = '.'  # 定义方块形状模板时,填补空白处的字符
MOVESIDEWAYSFREQ = 0.15  # 玩家一直按下左或右方向键，方块仍是每0.15s才会移动一次
MOVEDOWNFREQ = 0.1  # 玩家一直按下下方向键，方块仍是每0.1s移动一次
XMARGIN = (WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2  # 游戏界面的宽度
TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5  # 游戏界面的高度
# 定义RGB颜色变量
WHITE = (255, 255, 255)  # 设置字体颜色
BLACK = (0, 0, 0)  # 整个窗口背景为黑色
GRAY = (185, 185, 185)  # 字体阴影灰色
# 以下的颜色分别被COLORS和LIGHTCOLORS所引用
RED = (155, 0, 0)
LIGHTRED = (175, 20, 20)  # 浅红
GREEN = (0, 155, 0)
LIGHTGREEN = (20, 175, 20)  # 浅绿
BLUE = (0, 0, 155)
LIGHTBLUE = (20, 20, 175)  # 浅蓝
YELLOW = (155, 155, 0)
LIGHTYELLOW = (175, 175, 20)  # 浅黄
BORDERCOLOR = BLUE  # 游戏边框颜色为蓝色
BGCOLOR = BLACK  # 背景颜色为黑色
TEXTCOLOR = WHITE  # 字体为白色,被showTextScreen()引用,用来设置暂停时的"Pause"文本字体颜色
TEXTSHADOWCOLOR = GRAY  # 字体阴影为灰色,被showTextScreen()引用,用来设置暂停时的"Pause"文本阴影字体颜色
COLORS = (BLUE, GREEN, RED, YELLOW)  # 组成方块的小方块颜色
LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW)  # 围绕在小方块周边颜色，强调轮廓
assert len(COLORS) == len(LIGHTCOLORS)  # 每个颜色对应其浅色
TEMPLATEWIDTH = 5  # 模板宽
TEMPLATEHEIGHT = 5  # 模板高
S_SHAPE_TEMPLATE = [['.....',
                     '.....',
                     '..00.',
                     '.00..',
                     '.....'],
                    ['.....',
                     '..0..',
                     '..00.',
                     '...0.',
                     '.....']]
Z_SHAPE_TEMPLATE = [['.....',
                     '.....','.00..','..00.','.....'],
                    ['.....','..0..','.00..','.0...','.....']]
I_SHAPE_TEMPLATE = [['..0..',
                     '..0..',
                     '..0..',
                     '..0..',
                     '.....'],
                    ['.....',
                     '.....',
                     '0000.',
                     '.....',
                     '.....']]
O_SHAPE_TEMPLATE = [['.....','.....','.00..','.00..','.....']]
J_SHAPE_TEMPLATE = [['.....','.0...','.000.','.....','.....'],
                    ['.....','..00.','..0..','..0..','.....'],
                    ['.....','.....','.000.','...0.','.....'],
                    ['.....','..0..','..0..','.00..','.....']]
L_SHAPE_TEMPLATE = [['.....','...0.','.000.','.....','.....'],
                    ['.....','..0..','..0..','..00.','.....'],
                    ['.....','.....','.000.','.0...','.....'],
                    ['.....','.00..','..0..','..0..','.....']]
T_SHAPE_TEMPLATE = [['.....','..0..','.000.','.....','.....'],
                    ['.....','..0..','..00.','..0..','.....'],
                    ['.....','.....','.000.','..0..','.....'],
                    ['.....','..0..','.00..','..0..','.....']]
# 定义字典变量PIECES来存储所有的不同形状模板(重点是所有),即字典变量PIECES包含了每个类型的方块和所有的变换形状
PIECES = {'S': S_SHAPE_TEMPLATE,
          'Z': Z_SHAPE_TEMPLATE,
          'I': I_SHAPE_TEMPLATE,
          'O': O_SHAPE_TEMPLATE,
          'J': J_SHAPE_TEMPLATE,
          'L': L_SHAPE_TEMPLATE,
          'T': T_SHAPE_TEMPLATE}
# 主函数用于创建一些全局变量并在游戏开始之前显示一个初始画面
def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
    pygame.init()  # 初始化pygame相关模块，为使用硬件做准备
    FPSCLOCK = pygame.time.Clock()
    # 创建一个新对象，可以使用时钟跟踪的时间量。该时钟还提供多种功能以帮助控制游戏的帧率。返回一个Clock对象
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
    # 显示窗口(返回一个Surface对象)
    BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
    # 用来设置暂停时"Press a key to play."字体颜色,被showTextScreen()引用
    BIGFONT = pygame.font.Font('freesansbold.ttf', 100)
    # 用来设置暂停时"Pause"文本及其阴影文本字体大小,被showTextScreen()引用
    pygame.display.set_caption('付老师俄罗斯方块')
    while True:
        # 随机播放背景音乐,mid音乐格式由MIDI继承而来。MID文件并不是一段录制好的音乐,而是记录
        # 声音的信息，然后告诉声卡如何再现音乐的一组指令，所以音乐播放的好坏因不同机器的声卡而不同
        # if random.randint(0, 1) == 0:  # 加入异常语句，没有mid音乐也可直接运行
            #try:
                #pygame.mixer.music.load('音乐.mid')
            #except:
                #pass
        #else:
            #try:
                #pygame.mixer.music.load('音乐.mid')
            #except:
                #pass
        #try:
            #pygame.mixer.music.play(-1, 0.0)  # 参数依次为无限循环播放，从音乐开头播放
        #except:
            #pass
        runGame()
        #try:
            #pygame.mixer.music.stop()
       # except:
            #pass
        showTextScreen('游戏 Over')
def runGame():
    """启动运行游戏函数"""
    board = getBlankBoard()  # 返回一个新的空白板数据结构
    lastMoveDownTime = time.time()
    lastMoveSidewaysTime = time.time()
    lastFallTime = time.time()
    # 按下方向键会将以下三个变量设置为None,上移变量用于翻转方块，故没有上移变量
    movingDown = False
    movingLeft = False
    movingRight = False
    score = 0
    level, fallFreq = calculateLevelAndFallFreq(score)
    fallingPiece = getNewPiece()  # 当前掉落的方块
    nextPiece = getNewPiece()  # 游戏玩家可以在屏幕的NEXT区域看见的下一个方块
    while True:  # 开始游戏循环
        if fallingPiece == None:
            fallingPiece = nextPiece
            # 没有下降的一块在运动，所以开始一个新的一块在顶部,把nextPiece变量中的下一个方块赋值给fallingPiece变量
            nextPiece = getNewPiece()
            lastFallTime = time.time()
            # 重置lastFallTime变量,赋值为当前时间，这样就可以通过变量fallFreq控制方块下落频率
            if not isValidPosition(board, fallingPiece):
                # 不能在游戏框中放下新的方块，游戏结束，Valid:有效的，Position:位置
                return  # 返回runGame函数调用处
        checkForQuit()  # 不断检查是否要退出
        for event in pygame.event.get():  # 事件处理循环
            if event.type == KEYUP:
                if event.key == K_p:  # 暂停游戏
                    DISPLAYSURF.fill(BGCOLOR)  # 将DISPLAYSURF(窗口Surface对象)重新填充为黑色
                    showTextScreen('Paused')  # DISPLAYSURF(窗口Surface对象)显示暂停字样
                    lastMoveDownTime = time.time()
                    lastMoveSidewaysTime = time.time()
                    lastFallTime = time.time()
                elif event.key == K_LEFT or event.key == K_a:
                    movingLeft = False
                elif event.key == K_RIGHT or event.key == K_d:
                    movingRight = False
                elif event.key == K_DOWN or event.key == K_s:
                    movingDown = False
            elif event.type == KEYDOWN:  # 将方块侧向移动
                if event.key == K_LEFT or event.key == K_a and isValidPosition(board, fallingPiece, adjX=-1):
                    fallingPiece['x'] -= 1
                    movingLeft = True
                    movingRight = False
                    lastMoveSidewaysTime = time.time()
                elif event.key == K_RIGHT or event.key == K_d and isValidPosition(board, fallingPiece, adjX=1):
                    fallingPiece['x'] += 1
                    movingRight = True
                    movingLeft = False
                    lastMoveSidewaysTime = time.time()
                elif event.key == K_UP or event.key == K_w:  # 按向上键，旋转方块(如果有空间的话)
                    fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])
                    if not isValidPosition(board, fallingPiece):
                        fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
                elif event.key == K_q:  # 按q键，按相反方向旋转
                    fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
                    if not isValidPosition(board, fallingPiece):
                        fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])
                elif event.key == K_DOWN or event.key == K_s:
                    # 按下向下键，使得方块下落得更快,fallingPiece['y']+=1使方块下落一个格子,
                    # 前提是这是一个有效下落，把movingDown设置为True,把lastMoveDownTime变量
                    # 设置为当前时间.当向下方向键一直pp按下时，以后将会检查这个变量保证方块以一个较快速率下降
                    movingDown = True
                    if isValidPosition(board, fallingPiece, adjY=1):
                        fallingPiece['y'] += 1
                    lastMoveDownTime = time.time()
                elif event.key == K_SPACE:
                    # 如果按下的是空格，则将方块一步到底部。程序首先需要找出它着落需要下降多少个格子，有关moving的3个变量都要
                    # 设置为False,用以保证程序后面部分代码知道游戏玩家已经按下所有方向键。
                    movingDown = False
                    movingLeft = False
                    movingRight = False
                    for i in range(1, BOARDHEIGHT):
                        if not isValidPosition(board, fallingPiece, adjY=i):
                            break
                    fallingPiece['y'] += i - 1
        if (movingLeft or movingRight) and (
                time.time() - lastMoveSidewaysTime) > MOVESIDEWAYSFREQ:
            # 如果用户按住按键超过0.15s,那么该表达式返回True,便可使得方块左或右移一个格子
            # 因为如果用户重复按下方向键让方块移动多个格子是很烦人的。好的做法便是用户按下
            # 方向键让方块保持移动，直到松开键为止
            if movingLeft and isValidPosition(board, fallingPiece, adjX=-1):
                fallingPiece['x'] -= 1
            elif movingRight and isValidPosition(board, fallingPiece, adjX=1):
                fallingPiece['x'] += 1
            lastMoveSidewaysTime = time.time()
        if (movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ
                and isValidPosition(board, fallingPiece,adjY=1)):
            fallingPiece['y'] += 1
            lastMoveDownTime = time.time()
        if time.time() - lastFallTime > fallFreq:  # 让方块自动落下如果它到了下落的时候
            # 判断方块是否落地
            if not isValidPosition(board, fallingPiece, adjY=1):  # 方块已落地
                addToBoard(board, fallingPiece)  # 将落地的方块添加到游戏框中
                score += removeCompleteLines(board)  # 判断游戏框中的每一行是否填满,删除填满的一行,返回填满数
                level, fallFreq = calculateLevelAndFallFreq(score)  # 计算游戏等级以及下落频率
                fallingPiece = None  # 将当前下落的方块设置为空对象，然后在函数开头重新赋值nextPiece
            else:  # 方块没有落地,只是把方块移下来
                fallingPiece['y'] += 1
                lastFallTime = time.time()
        # 把所有东西画在屏幕上
        DISPLAYSURF.fill(BGCOLOR)  # 设置窗口背景颜色
        drawBoard(board)  # 将空白数据模板绘画到窗口
        drawStatus(score, level)  # 将状态栏绘画到窗口
        drawNextPiece(nextPiece)  # 将下一个方块显示栏画到窗口
        if fallingPiece != None:
            drawPiece(fallingPiece)
        pygame.display.update()
        FPSCLOCK.tick(FPS)
def getBlankBoard():
    board = []
    for i in range(BOARDWIDTH):
        board.append([BLANK] * BOARDHEIGHT)
    return board
def calculateLevelAndFallFreq(score):
    """根据分数，返回玩家所处的关卡，以及掉落的棋子掉落到一个空格前的时间间隔。"""
    level = int(score / 10) + 1  # 等级
    fallFreq = 0.27 - (level * 0.02)  # 下落频率(frequence)
    return level, fallFreq
def getNewPiece():
    """以随机旋转和颜色返回随机的新块"""
    shape = random.choice(list(PIECES.keys()))  # shape为S,Z,I....
    newPiece = {'shape': shape,
                'rotation': random.randint(0, len(PIECES[shape]) - 1),
                # PIECES[shape]为S_SHAPE_TEMPLATE,Z_SHAPE_TEMPLATE,I_SHAPE_TEMPLATE.此处指一类方块的可旋转次数
                'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2),
                'y': -2,
                'color': random.randint(0, len(COLORS) - 1)}
    return newPiece
def checkForQuit():
    """检查退出事件"""
    for event in pygame.event.get(QUIT):  # 获取所有退出事件
        terminate()  # 如果存在任何退出事件，则调用终止函数
    for event in pygame.event.get(KEYUP):  # 获取所有的放开键事件
        if event.key == K_ESCAPE:  # 如果KEYUP事件是针对Esc键的
            terminate()  # 终止
        pygame.event.post(event)  # 将其他KEYUP(非Esc键放开)事件对象放回事件队列
def terminate():
    """退出函数"""
    pygame.quit()
    sys.exit()
def showTextScreen(text):
    """在窗口Surface对象的中央显示大文本，直到按下一个键。用于绘制文本投影,注意：pygame没有提供直接在现有
    表面绘制文本的方法:相反，您必须使用Font.render()来创建文本的图像(表面)，然后将此图像blit到另一个表面。
    """
    # 做出阴影效果
    titleSurf, titleRect = makeTextObjs(text, BIGFONT,
                                        TEXTSHADOWCOLOR)
    # BIGFONT为main函数中定义，TEXTSHADOWCOLOR为模块中定义,此处文本颜色TEXTSHADOWCOLOR(灰色)，下面是TEXTCOLOR(白色)
    titleRect.center = (
    int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
    # 设置矩形容器的位置，此处需要注意titleRect指的是每个文本Surface对象与其对应的全覆盖矩形容器,此容器在这里不可见
    DISPLAYSURF.blit(titleSurf, titleRect)
    # 在窗口Surface对象画文本Surface对象和与其对应的矩形Surface对象,做出blit这个动作的人是一个Surface类的实例
    # 实体文本整体向左上移动3个单位,与上面的阴影效果唯一不同的就是传入的颜色参数
    titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR)
    titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)
    DISPLAYSURF.blit(titleSurf, titleRect)
    # 绘制"Paused"文本下附加的"Press a key to play."文本
    pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.', BASICFONT, TEXTCOLOR)
    pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)
    DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
    while checkForKeyPress() == None:
        pygame.display.update()
        FPSCLOCK.tick()
        # 这种方法应该被调用一次。它将计算多少毫秒被调用。如果可选的帧率参数的函数以将延迟保持在低于给
        # 定游戏运行时每秒滴答。这有助于限制运行游戏的速度。通过调用clock.tick每帧一次地(40)，该程序不会运行超过40帧/秒。
def makeTextObjs(text, font, color):
    """这将创建一个新Surface对象，并在其上呈现指定的文本。"""
    surf = font.render(text, True, color)
    # 参数依次为：要写的文本，布尔值（是否开启抗锯齿功能，True字体较为平滑，），字体颜色，背景色
    return surf, surf.get_rect()
    # 返回一个覆盖整个表面的新矩形(Rect对象,一个矩形的灵活容器)
def isValidPosition(board, piece, adjX=0, adjY=0):
    """如果块在板内且没有碰撞，则返回True，该函数我暂时不理解"""
    for x in range(TEMPLATEWIDTH):
        for y in range(TEMPLATEHEIGHT):
            isAboveBoard = y + piece['y'] + adjY < 0
            if isAboveBoard or PIECES[piece['shape']][piece['rotation']][y][x] == BLANK:
                continue
            if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY):
                return False
            if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK:
                return False
    return True
def isOnBoard(x, y):
    return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT
def addToBoard(board, piece):
    """根据方块的位置，形状，旋转填充的游戏框中"""
    for x in range(TEMPLATEWIDTH):
        for y in range(TEMPLATEHEIGHT):
            if PIECES[piece['shape']][piece['rotation']][y][x] != BLANK:
                board[x + piece['x']][y + piece['y']] = piece['color']
def removeCompleteLines(board):
    """删除游戏框中所有已完成(Completed)的行,将上面的所有内容向下移动,并返回完整行数。"""
    numLinesRemoved = 0  # 记录删除的行数
    y = BOARDHEIGHT - 1  # 从游戏框底部开始
    while y >= 0:
        if isCompleteLine(board, y):  # 如果填充了这一行,则移开这条线，把所有方块往下一拉
            for pullDownY in range(y, 0, -1):
                for x in range(BOARDWIDTH):
                    board[x][pullDownY] = board[x][pullDownY - 1]
            for x in range(BOARDWIDTH):  # 将最上面一行设置为空白
                board[x][0] = BLANK
            numLinesRemoved += 1
        else:
            y -= 1  # 继续查看下一行
    return numLinesRemoved
def isCompleteLine(board, y):
    """如果行中填充了没有空格的框，则返回True"""
    for x in range(BOARDWIDTH):
        if board[x][y] == BLANK:
            return False
    return True
def calculateLevelAndFallFreq(score):
    """基于分数，返回玩家所在的关卡。"""
    level = int(score / 10) + 1
    fallFreq = 0.27 - (level * 0.02)  # 根据level变量计算方块每下落一个空间所需要的秒数
    return level, fallFreq
def drawBoard(board):
    """将空白数据模板画到窗口Surface对象上"""
    pygame.draw.rect(DISPLAYSURF, BORDERCOLOR,
                     (XMARGIN - 3, TOPMARGIN - 7,
                      (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5)
    # fill the background of the board #填充空白数据模板背景颜色
    pygame.draw.rect(DISPLAYSURF, BGCOLOR,
                     (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT))
    # draw the individual boxes on the board #在空白数据模板上画出各个盒子
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            drawBox(x, y, board[x][y])
def drawStatus(score, level):
    """在Surface窗口对象上画在游戏框旁边的状态栏"""
    # 绘制分数文本
    scoreSurf = BASICFONT.render("Score:%s" % score, True, TEXTCOLOR)  # TEXTCOLOR为白色
    scoreRect = scoreSurf.get_rect()
    scoreRect.topleft = (WINDOWWIDTH - 150, 20)
    DISPLAYSURF.blit(scoreSurf, scoreRect)
    # 绘制等级文本
    levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR)
    levelRect = levelSurf.get_rect()
    levelRect.topleft = (WINDOWWIDTH - 150, 50)
    DISPLAYSURF.blit(levelSurf, levelRect)
def drawNextPiece(piece):
    # 绘制"下一个"文本
    nextSurf = BASICFONT.render("Next:", True, TEXTCOLOR)
    nextRect = nextSurf.get_rect()
    nextRect.topleft = (WINDOWWIDTH - 120, 80)
    DISPLAYSURF.blit(nextSurf, nextRect)
    # 绘制下一个"方块",pixel中文为像素
    drawPiece(piece, pixelx=WINDOWWIDTH - 120, pixely=100)
def drawPiece(piece, pixelx=None, pixely=None):
    shapeToDraw = PIECES[piece['shape']][piece['rotation']]
    if pixelx == None and pixely == None:
        pixelx, pixely = convertToPixelCoords(piece['x'], piece['y'])
        # 如果没有指定pixelx & pixely，则使用存储在片段数据结构中的位置
    # 画出组成这个方块的每一个小方块
    for x in range(TEMPLATEWIDTH):
        for y in range(TEMPLATEHEIGHT):
            if shapeToDraw[y][x] != BLANK:
                drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE))
def convertToPixelCoords(boxx, boxy):
    """将board上给定的xy坐标转换为屏幕上位置的xy坐标,convert:转换,pixel:像素,coords:坐标"""
    return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE))
def drawBox(boxx, boxy, color, pixelx=None, pixely=None):
    """在黑板上的xy坐标处画一个盒子(每个四重奏有四个盒子)。
    或者，如果指定了pixelx & pixely，则绘制到存储在pixelx & pixely中的像素坐标(这用于“下一个”块)"""
    if color == BLANK:
        return
    if pixelx == None and pixely == None:
        pixelx, pixely = convertToPixelCoords(boxx, boxy)
    pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1))
    pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4))
def checkForKeyPress():
    """遍历事件队列，寻找KEYUP事件,获取KEYDOWN事件，将它们从事件队列中删除"""
    checkForQuit()
    for event in pygame.event.get([KEYDOWN, KEYUP]):
        if event.type != KEYDOWN:
            continue
        return event.key
    return None
if __name__ == '__main__':
    main()